날짜 및 시간 API
1. 개요
1. 개요
날짜 및 시간 API는 자바 프로그래밍 언어에서 날짜와 시간을 다루기 위한 표준 API이다. 이 API는 자바 SE 8에서 처음 도입되었으며, 기존의 java.util.Date 클래스와 java.util.Calendar 클래스의 여러 문제점을 해결하기 위해 설계되었다. Oracle이 제공하는 이 API는 날짜와 시간 연산을 보다 직관적이고 안전하게 수행할 수 있도록 지원한다.
주요 용도는 애플리케이션에서 날짜 생성, 시간 계산, 형식화, 파싱 등 다양한 시간 관련 기능을 구현하는 것이다. 이는 소프트웨어 개발 전반, 특히 물류, 금융, 예약 시스템 등 시간에 민감한 비즈니스 로직이 필요한 분야에서 널리 활용된다. 새로운 API는 불변 객체를 사용하여 스레드 안전성을 보장하고, 명확하게 분리된 클래스 체계를 통해 코드의 가독성과 유지보수성을 크게 향상시켰다.
2. 기존 API의 문제점
2. 기존 API의 문제점
2.1. java.util.Date 클래스
2.1. java.util.Date 클래스
java.util.Date 클래스는 자바 초기 버전부터 제공된 날짜와 시간을 표현하는 핵심 클래스이다. 이 클래스는 특정 시점을 1970년 1월 1일 00:00:00 UTC를 기준으로 경과한 밀리초 단위로 표현한다. 그러나 설계상 여러 문제점을 안고 있어 개발자들에게 많은 불편함을 초래했다.
가장 큰 문제점은 변경 가능성이다. Date 객체는 생성 후에도 내부 상태를 변경할 수 있는 가변 객체이기 때문에, 멀티스레드 환경에서 안전하지 않다. 또한 API 설계가 직관적이지 않아 오용하기 쉽다. 예를 들어, 월을 표현할 때는 1월이 0으로 시작하며, 연도는 1900년을 기준으로 오프셋을 더해 표현한다. 이로 인해 new Date(117, 8, 21)과 같은 코드는 실제로 2017년 9월 21일을 의미하게 되어 코드 가독성을 현저히 떨어뜨린다.
이러한 한계로 인해 java.util.Date는 복잡한 날짜 연산이나 형식화, 파싱에 부적합했으며, 많은 개발자들이 Joda-Time 같은 서드파티 라이브러리를 사용해야 했다. 결국 자바 8에서는 이러한 문제들을 근본적으로 해결한 새로운 java.time 패키지가 도입되었고, Date 클래스는 공식적으로 구식 API로 분류되어 새로운 프로젝트에서의 사용이 권장되지 않는다.
2.2. java.util.Calendar 클래스
2.2. java.util.Calendar 클래스
java.util.Calendar 클래스는 java.util.Date 클래스의 문제점을 보완하기 위해 도입된 추상 클래스이다. 이 클래스는 날짜와 시간을 계산하고 조작하는 기능을 제공하지만, 여전히 여러 가지 설계상의 문제점을 안고 있다. 가장 큰 문제는 가변 객체라는 점으로, 인스턴스의 상태를 변경할 수 있어 스레드 안전성이 보장되지 않는다. 또한, 월을 표현할 때 0부터 시작하는 인덱스를 사용하는 등 직관적이지 않은 API 설계로 인해 개발자에게 혼란을 준다.
이 클래스는 정적 메서드인 getInstance()를 통해 특정 시간대와 로케일 설정에 맞는 구체적인 서브클래스 인스턴스를 생성한다. 이를 통해 그레고리력 외에도 다양한 역법을 지원할 수 있지만, 실제로는 대부분의 경우 GregorianCalendar 구현체가 사용된다. 날짜 필드의 값을 가져오거나 설정하기 위해 get() 및 set() 메서드를 사용하며, add()나 roll() 메서드를 이용한 날짜 연산이 가능하다.
그러나 복잡한 시간대 변환이나 일광 절약 시간 처리가 어렵고, 코드 가독성이 떨어지는 단점이 있다. 이러한 한계로 인해 많은 개발자들이 Joda-Time 같은 서드파티 라이브러리를 사용했으며, 궁극적으로 Java SE 8에서 이 모든 문제를 해결한 현대적인 java.time 패키지가 등장하게 되었다.
2.3. DateFormat 클래스
2.3. DateFormat 클래스
java.text.DateFormat 클래스는 java.util.Date 객체와 java.util.Calendar 객체를 사람이 읽을 수 있는 문자열로 변환하거나, 반대로 문자열을 날짜 객체로 변환하는 기능을 제공한다. 이 클래스는 날짜와 시간의 형식을 로케일에 맞춰 국제화하는 데 사용된다. DateFormat 클래스 자체는 추상 클래스이며, 주로 그 하위 클래스인 SimpleDateFormat을 통해 구체적인 패턴을 지정하여 사용한다.
그러나 DateFormat 클래스는 몇 가지 심각한 문제점을 안고 있다. 첫째, 이 클래스는 스레드 안전성이 보장되지 않는다. DateFormat 인스턴스는 내부 상태를 변경할 수 있는 가변 객체이므로, 여러 스레드가 동시에 하나의 인스턴스를 사용하면 예기치 않은 결과가 발생할 수 있다. 이 문제를 해결하기 위해 매번 새로운 인스턴스를 생성하거나 동기화 처리를 해야 하는 번거로움이 있다. 둘째, API 설계가 직관적이지 않다. java.util.Date와 같이 설계상 결함이 있는 레거시 클래스에 강하게 결합되어 있어 사용법이 복잡하다. 셋째, 오류 처리도 취약하여 잘못된 형식의 문자열을 파싱할 때 예외를 던지기보다는 조용히 잘못된 결과를 반환하는 경우가 있어 디버깅이 어려울 수 있다.
이러한 DateFormat 클래스의 한계는 새로운 java.time 패키지의 등장으로 극복되었다. java.time.format.DateTimeFormatter 클래스는 불변 객체로 설계되어 스레드 안전하며, 훨씬 풍부하고 직관적인 형식화 및 파싱 기능을 제공한다. 또한 LocalDate나 LocalDateTime 같은 현대적인 날짜-시간 클래스와 완벽하게 통합되어 작업이 간소화된다.
3. java.time 패키지
3. java.time 패키지
3.1. LocalDate, LocalTime, LocalDateTime
3.1. LocalDate, LocalTime, LocalDateTime
java.time 패키지의 핵심 클래스인 LocalDate, LocalTime, LocalDateTime은 시간대 정보를 포함하지 않는 불변 객체이다. 이들은 각각 순수한 날짜, 순수한 시간, 그리고 날짜와 시간의 조합을 표현하는 데 사용된다. 기존의 java.util.Date와 java.util.Calendar 클래스가 가졌던 가변성과 혼란스러운 API 설계 문제를 해결하여, 직관적이고 오류 발생 가능성이 낮은 프로그래밍을 가능하게 한다.
LocalDate 클래스는 연, 월, 일 정보만을 담아 생년월일이나 기념일과 같은 순수 날짜 개념을 표현한다. now() 메서드로 현재 날짜를, of(int year, int month, int dayOfMonth) 메서드로 특정 날짜를 생성할 수 있다. LocalTime 클래스는 시, 분, 초, 나노초 정보를 담아 알람 시간이나 영업 시간과 같은 순수 시간 개념을 표현한다. of(int hour, int minute) 등의 메서드를 통해 시간을 생성한다.
LocalDateTime은 LocalDate와 LocalTime을 결합한 클래스로, 시간대 정보 없이 지역적인 날짜와 시간을 모두 표현해야 할 때 사용된다. 회의 일정이나 시스템 로그의 타임스탬프와 같은 경우에 적합하다. LocalDate 객체에 atTime() 메서드를 적용하거나, LocalTime 객체에 atDate() 메서드를 적용하여 생성할 수 있으며, toLocalDate()와 toLocalTime() 메서드로 각 구성 요소를 다시 추출할 수 있다. 이들 클래스는 모두 불변이므로, plusDays(), minusHours(), withYear() 등의 메서드를 호출하면 원본 객체를 변경하는 대신 새로운 객체가 반환된다.
3.2. Instant
3.2. Instant
Instant 클래스는 유닉스 시간의 에포크인 1970년 1월 1일 00:00:00 UTC를 기준으로 경과된 시간을 나노초 단위까지 정밀하게 표현한다. 이는 기계가 이해하기 위한 시간 표현으로, 시스템의 현재 시간을 타임스탬프로 기록하거나 두 시점 사이의 정확한 시간 간격을 계산할 때 주로 사용된다. Instant는 시간대 정보를 포함하지 않으며, 항상 UTC를 기준으로 하기 때문에 전 세계 어디서나 동일한 절대적 시점을 나타낸다.
주요 생성 방법으로는 현재 시점을 얻는 Instant.now() 메서드와, 에포크로부터 경과된 초(와 선택적으로 나노초)를 지정하여 생성하는 Instant.ofEpochSecond() 메서드가 있다. 또한 기존의 java.util.Date 객체와 상호 변환이 가능하여 레거시 코드와의 호환성을 제공한다. Instant는 불변 객체이므로 모든 연산은 새로운 객체를 반환한다.
이 클래스는 주로 애플리케이션 로그의 타임스탬프 기록, 성능 측정을 위한 실행 시간 계산, 또는 분산 시스템 간의 이벤트 발생 순서를 비교할 때 유용하게 활용된다. 시간대나 달력 체계에 의존하지 않는 순수한 시간의 '순간'을 표현해야 할 때 Instant를 사용하는 것이 적합하다.
3.3. Duration과 Period
3.3. Duration과 Period
Duration 클래스는 두 시간 사이의 지속 시간을 초와 나노초 단위로 표현한다. 이 클래스는 시간 기반의 간격을 계산할 때 사용되며, LocalTime, LocalDateTime, Instant와 같은 시간 정보를 포함하는 객체 간의 차이를 구하는 데 적합하다. 예를 들어, 작업 시작 시간과 종료 시간 사이의 경과 시간을 계산하거나, 특정 프로세스의 소요 시간을 측정하는 용도로 활용된다. Duration.between() 정적 메서드를 사용하여 간편하게 두 시간 객체의 차이를 구할 수 있다.
반면 Period 클래스는 두 날짜 사이의 기간을 년, 월, 일 단위로 표현한다. 이 클래스는 날짜 기반의 간격을 다루며, LocalDate 객체 간의 차이를 계산하는 데 사용된다. 생년월일로부터 현재까지의 나이를 계산하거나, 계약 시작일부터 종료일까지의 기간을 구하는 등의 시나리오에서 유용하다. Period.between() 메서드를 통해 두 LocalDate 객체 사이의 기간을 쉽게 얻을 수 있다.
두 클래스 모두 불변 객체의 특성을 가지므로, 한 번 생성된 인스턴스의 값을 변경할 수 없다. 모든 계산 메서드는 새로운 Duration이나 Period 객체를 반환한다. 또한, Duration과 Period는 각각 plus, minus, multipliedBy, negated와 같은 메서드를 제공하여 기간에 대한 다양한 산술 연산을 지원한다. 이를 통해 복잡한 날짜 및 시간 계산을 안전하고 명확하게 처리할 수 있다.
3.4. DateTimeFormatter
3.4. DateTimeFormatter
DateTimeFormatter는 java.time 패키지의 핵심 클래스 중 하나로, 날짜와 시간 객체를 사람이 읽을 수 있는 문자열로 변환하거나, 반대로 문자열을 날짜와 시간 객체로 변환하는 역할을 한다. 이전에 사용되던 SimpleDateFormat 클래스와 유사한 기능을 제공하지만, 스레드 안전하고 불변 객체라는 점에서 근본적으로 다르다.
DateTimeFormatter는 미리 정의된 상수 패턴을 제공하며, 사용자가 직접 패턴 문자열을 지정하여 생성할 수도 있다. 미리 정의된 포맷터로는 ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME 등이 있으며, 이들은 ISO 8601 표준 형식을 따른다. 또한 ofPattern 메서드를 사용하면 "yyyy-MM-dd HH:mm:ss"와 같은 사용자 정의 패턴으로 포맷터를 만들 수 있다. 이 클래스는 로케일 설정도 지원하여, 특정 지역의 언어와 형식에 맞춰 날짜와 시간을 표현할 수 있게 해준다.
주요 사용법은 format 메서드를 이용해 LocalDate, LocalDateTime 등의 객체를 문자열로 변환하거나, parse 메서드를 이용해 문자열을 해당 날짜 시간 객체로 변환하는 것이다. 이를 통해 애플리케이션의 로깅, 사용자 인터페이스 표시, 또는 외부 시스템과의 데이터 교환 시 일관된 형식을 유지할 수 있다.
4. 사용 방법 예시
4. 사용 방법 예시
4.1. 날짜 생성 및 조작
4.1. 날짜 생성 및 조작
java.time API에서 날짜와 시간 객체를 생성하는 방법은 직관적이고 다양하다. 가장 일반적인 방법은 정적 팩토리 메서드 now()와 of()를 사용하는 것이다. LocalDate.now()는 현재 시스템의 날짜를, LocalTime.now()는 현재 시간을 반환한다. 특정 날짜나 시간을 생성하려면 LocalDate.of(2023, 5, 30) 또는 LocalTime.of(14, 30, 45)와 같이 of() 메서드에 구성 요소를 직접 지정하면 된다. LocalDateTime은 LocalDate와 LocalTime을 조합하여 LocalDateTime.of(date, time) 방식으로 생성하거나, LocalDate.atTime() 메서드를 사용할 수도 있다.
생성된 불변(Immutable Object) 객체는 with, plus, minus 메서드를 통해 조작된다. withYear(2024)와 같은 with 메서드는 특정 필드의 값을 절대적으로 변경한 새로운 객체를 반환한다. plusDays(7)이나 minusMonths(1)과 같은 plus/minus 메서드는 상대적인 시간을 더하거나 뺀 결과를 제공한다. 이러한 메서드들은 체이닝이 가능하여 today.plusWeeks(1).minusDays(2)와 같은 복합 연산을 간결하게 표현할 수 있다.
또한 ISO 8601 형식의 문자열을 파싱하여 객체를 생성할 수 있다. LocalDate.parse("2023-05-30") 또는 LocalDateTime.parse("2023-05-30T15:45:30")을 사용하면 된다. 사용자 정의 형식이 필요할 경우 DateTimeFormatter를 함께 사용한다. 날짜와 시간의 조합을 분리할 때는 LocalDateTime.toLocalDate() 및 toLocalTime() 메서드를 활용한다.
4.2. 형식화 및 파싱
4.2. 형식화 및 파싱
java.time API에서는 DateTimeFormatter 클래스를 사용하여 날짜와 시간 객체를 특정 형식의 문자열로 변환하거나, 문자열을 날짜와 시간 객체로 변환할 수 있다. 이 클래스는 기존의 SimpleDateFormat 클래스와 달리 불변 객체이며 스레드 안전하다는 장점이 있다.
DateTimeFormatter는 미리 정의된 상수 패턴을 제공한다. 예를 들어, ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME 등을 사용하여 표준 ISO 8601 형식으로 쉽게 변환할 수 있다. 또한 ofPattern 메서드를 사용하면 개발자가 직접 "yyyy-MM-dd HH:mm:ss"와 같은 커스텀 패턴을 지정하여 형식화와 파싱을 수행할 수 있다. 형식화는 format 메서드를, 파싱은 parse 메서드를 사용한다. 특히 parse 메서드는 문자열과 포매터를 인자로 받아 LocalDate, LocalTime, LocalDateTime 등의 객체를 생성해 반환한다.
로케일을 지원하는 것도 중요한 기능이다. withLocale 메서드를 사용하면 특정 지역의 언어와 규칙에 맞춰 날짜와 시간을 표현할 수 있다. 예를 들어, 한국어 로케일을 적용하면 "오후 03시 30분"과 같은 형식으로 출력이 가능하다. 이를 통해 국제화된 애플리케이션을 더 쉽게 개발할 수 있다.
4.3. 시간 간격 계산
4.3. 시간 간격 계산
java.time API에서는 두 시점 사이의 시간 간격을 계산하기 위해 Duration과 Period 클래스를 제공한다. 이 두 클래스는 각각 다른 단위의 시간 간격을 표현하도록 설계되었다.
Duration 클래스는 시간 기반의 간격을 표현한다. 이 클래스는 LocalTime, LocalDateTime, Instant와 같은 시간 정보를 포함하는 객체 사이의 차이를 계산할 때 사용된다. 예를 들어, Duration.between(startTime, endTime) 메서드를 호출하면 두 LocalTime 객체 사이의 시, 분, 초 차이를 Duration 객체로 얻을 수 있다. 이 객체의 toHours(), toMinutes(), getSeconds() 같은 메서드를 통해 간격을 다양한 시간 단위로 확인할 수 있다.
반면, Period 클래스는 날짜 기반의 간격을 표현한다. 이 클래스는 LocalDate 객체 사이의 차이, 즉 연, 월, 일 단위의 기간을 계산할 때 사용된다. Period.between(startDate, endDate) 메서드를 사용하면 두 날짜 사이의 전체 기간이 Period 객체로 반환된다. 이후 getYears(), getMonths(), getDays() 메서드를 호출하여 각 구성 요소를 개별적으로 추출할 수 있다.
간격 계산 시 주의할 점은 Duration은 순수한 날짜(LocalDate) 간의 차이를 계산할 수 없다는 것이다. 날짜 간의 전체 일 수를 알고 싶다면, Period를 사용하거나 ChronoUnit.DAYS.between() 같은 메서드를 활용할 수 있다. 이렇게 Duration과 Period를 상황에 맞게 구분하여 사용하면 `java.util.Date`와 `Calendar`를 사용할 때보다 훨씬 명확하고 정확한 시간 간격 계산이 가능해진다.
5. 기타 클래스 및 기능
5. 기타 클래스 및 기능
5.1. ZonedDateTime
5.1. ZonedDateTime
ZonedDateTime은 java.time 패키지에서 시간대와 지역 정보를 포함한 날짜와 시간을 표현하는 클래스이다. LocalDateTime이 시간대 정보를 포함하지 않는 '현지 시간'을 나타낸다면, ZonedDateTime은 특정 지역의 시간대 규칙(예: 일광 절약 시간)을 적용한 실제 시점을 나타낸다. 이는 국제적인 약속이나 서버 로그처럼 전 세계적으로 통일된 시점을 기록해야 할 때 필수적이다.
ZonedDateTime 객체는 now() 메서드로 현재 시스템의 시간대 정보와 함께 생성하거나, of() 메서드에 연도, 월, 일, 시간, 분, 초 및 시간대 ID(예: "Asia/Seoul")를 지정하여 생성할 수 있다. 또한 withZoneSameInstant() 메서드를 사용하면 동일한 시점을 다른 시간대의 관점에서 쉽게 변환할 수 있다. 예를 들어, 서울 시간을 뉴욕 시간으로 변환하는 작업이 간단해진다.
메서드 | 설명 |
|---|---|
| 현재 시스템의 시간대 정보로 객체 생성 |
| 지정된 구성 요소와 시간대로 객체 생성 |
| 동일한 시점을 다른 시간대로 변환 |
이 클래스는 일광 절약 시간 전환 같은 복잡한 시간대 문제를 자동으로 처리해주므로, 개발자가 직접 계산할 필요가 없다. 따라서 국제화가 필요한 애플리케이션이나 분산 시스템에서 시간을 정확하게 다루는 데 핵심적인 역할을 한다.
5.2. TemporalAdjusters
5.2. TemporalAdjusters
TemporalAdjusters는 java.time 패키지에서 제공하는 유틸리티 클래스로, 복잡한 날짜 조정 작업을 간편하게 수행할 수 있도록 미리 정의된 조정기(TemporalAdjuster)를 제공한다. 이 클래스는 주로 LocalDate와 같은 Temporal 객체의 with 메서드와 함께 사용되어, 특정 규칙에 맞는 날짜를 계산할 때 유용하다.
주요 기능으로는 특정 요일의 날짜를 찾거나, 월 또는 연도의 시작일 및 마지막 날을 계산하는 것이 포함된다. 예를 들어, firstDayOfMonth(), lastDayOfYear(), next(DayOfWeek) 등의 정적 메서드를 통해 "다음 월요일", "이번 달의 마지막 날", "올해 첫 번째 금요일"과 같은 날짜를 쉽게 얻을 수 있다. 이를 통해 개발자는 반복적이고 오류 발생 가능성이 있는 수동 날짜 계산 로직을 작성할 필요가 없어진다.
또한 TemporalAdjuster 인터페이스를 직접 구현하여 사용자 정의 조정 로직을 만들 수도 있다. TemporalAdjusters의 ofDateAdjuster 메서드를 사용하면 람다 표현식으로 간단하게 다음 영업일 계산이나 특정 비즈니스 규칙에 따른 날짜 조정기 등을 정의할 수 있어 매우 유연하다. 이는 기존의 java.util.Calendar를 사용할 때보다 훨씬 명확하고 안전한 날짜 조작을 가능하게 한다.
6. 여담
6. 여담
java.time API는 자바 8에서 도입된 현대적인 날짜와 시간 처리 라이브러리이다. 이 API는 기존의 java.util.Date와 java.util.Calendar 클래스가 가진 여러 문제점을 해결하기 위해 설계되었다. 그 핵심 철학은 불변성, 명확한 분리, 그리고 풍부한 기능을 제공하는 데 있다. 이 API의 설계에는 Joda-Time 라이브러리의 영향이 크게 반영되어 있으며, 많은 개발자들이 오랫동안 사용해 온 검증된 개념을 자바 표준 라이브러리로 편입시킨 사례이다.
다른 프로그래밍 언어와의 비교에서도 java.time API의 장점은 두드러진다. 예를 들어, 자바스크립트의 전통적인 Date 객체는 변경 가능하고 타임존 처리가 혼란스러운 반면, java.time의 클래스들은 모두 불변 객체이며 ZonedDateTime 등을 통해 타임존을 명시적이고 안전하게 다룰 수 있다. C#의 DateTime 구조체나 파이썬의 datetime 모듈과 비교했을 때도, 자바의 API는 날짜(LocalDate), 시간(LocalTime), 날짜-시간(LocalDateTime), 순간(Instant) 등 개념을 명확히 분리하고 각각에 특화된 연산을 제공한다는 점에서 차별화된다.
이 API는 단순한 날짜 계산을 넘어 국제 표준인 ISO 8601 형식을 완벽히 지원하며, 다양한 달력 시스템과 로케일을 고려할 수 있는 확장성을 갖추고 있다. 또한, 스레드 안전성을 보장하는 불변 객체 특성 덕분에 동시성 프로그래밍 환경에서도 안전하게 사용할 수 있다. 이러한 설계 원칙과 기능적 완성도는 java.time API가 단순한 유틸리티를 넘어 현대적 소프트웨어 개발의 필수 도구로 자리 잡게 한 이유이다.
